﻿#region copyright
// Halcyone - Silverlight Application Framework
// Copyright (C) 2009 - 2010 Alexey Zakharov (B&Z LLC)
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#endregion
namespace Witcraft.Halcyone.ServiceModel
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Browser;
    using System.Reflection;
    using System.Runtime.Serialization.Json;
    using System.Text;
    using System.Windows;

    /// <summary>
    /// 
    /// </summary>
    public static class Rest
    {
        #region Constants and Fields

        private const string JsonContentType = "application/json";

        #endregion

        #region Public Methods

        public static Func<IObservable<TResult>> GenerateOperation<TResult>(
             Func<IObservable<TResult>> func)
        {
            return () => GetObservableResult<TResult>(func.Method);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, IObservable<TResult>> GenerateOperation<T1, TResult>(
            Func<T1,IObservable<TResult>> func)
        {
            return (t1) => GetObservableResult<TResult>(func.Method, t1);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="T2"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, T2, IObservable<TResult>> GenerateOperation<T1, T2, TResult>(
            Func<T1, T2, IObservable<TResult>> func)
        {
            return (t1, t2) => GetObservableResult<TResult>(func.Method, t1, t2);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="T2"></typeparam>
        ///<typeparam name="T3"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, T2, T3, IObservable<TResult>> GenerateOperation<T1, T2, T3, TResult>(
            Func<T1, T2, T3, IObservable<TResult>> func)
        {
            return (t1, t2, t3) => GetObservableResult<TResult>(func.Method, t1, t2, t3);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="T2"></typeparam>
        ///<typeparam name="T3"></typeparam>
        ///<typeparam name="T4"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, T2, T3, T4, IObservable<TResult>> GenerateOperation<T1, T2, T3, T4, TResult>(
            Func<T1, T2, T3, T4, IObservable<TResult>> func)
        {
            return (t1, t2, t3, t4) => GetObservableResult<TResult>(func.Method, t1, t2, t3, t4);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="T2"></typeparam>
        ///<typeparam name="T3"></typeparam>
        ///<typeparam name="T4"></typeparam>
        ///<typeparam name="T5"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, T2, T3, T4, T5, IObservable<TResult>> GenerateOperation<T1, T2, T3, T4, T5, TResult>(
            Func<T1, T2, T3, T4, T5, IObservable<TResult>> func)
        {
            return (t1, t2, t3, t4, t5) => GetObservableResult<TResult>(func.Method, t1, t2, t3, t4, t5);
        }

        ///<summary>
        ///</summary>
        ///<param name="func"></param>
        ///<typeparam name="T1"></typeparam>
        ///<typeparam name="T2"></typeparam>
        ///<typeparam name="T3"></typeparam>
        ///<typeparam name="T4"></typeparam>
        ///<typeparam name="T5"></typeparam>
        ///<typeparam name="T6"></typeparam>
        ///<typeparam name="TResult"></typeparam>
        ///<returns></returns>
        public static Func<T1, T2, T3, T4, T5, T6, IObservable<TResult>> GenerateOperation<T1, T2, T3, T4, T5, T6, TResult>(
            Func<T1, T2, T3, T4, T5, T6, IObservable<TResult>> func)
        {
            return (t1, t2, t3, t4, t5, t6) => GetObservableResult<TResult>(func.Method, t1, t2, t3, t4, t5, t6);
        }

        #endregion

        #region Methods

        internal static bool CheckRequestParameter(IEnumerable<ParameterInfo> parameters)
        {
            int requestParamAttributeCount = 0;
            requestParamAttributeCount =
                parameters.Aggregate(
                    requestParamAttributeCount,
                    (s, p) =>
                        {
                            if (p.IsDefined(typeof(RequestParameterAttribute), false))
                            {
                                ++s;
                            }
                            return s;
                        });

            if (requestParamAttributeCount > 1)
            {
                throw new ArgumentException("You declared more than 1 request attribute");
            }

            return requestParamAttributeCount > 0;
        }

        //TODO: Refactoring, add unit tests

        internal static string CreateQueryString(IDictionary<string, string> parameters)
        {
            var builder = new StringBuilder();

            var counter = 0;

            foreach (var parameter in parameters)
            {
                builder.Append(
                    counter > 0
                        ? string.Format(CultureInfo.InvariantCulture, "&{0}={1}", parameter.Key, parameter.Value)
                        : string.Format(CultureInfo.InvariantCulture, "?{0}={1}", parameter.Key, parameter.Value));
                counter++;
            }

            return builder.ToString();
        }

        internal static IEnumerable<InfoValuePair> GetNameValuePairs(List<ParameterInfo> parameters, object[] paramValues)
        {
          return Enumerable.Zip(parameters,paramValues, (p, v) => new InfoValuePair() { Info = p, Value = v })
              .Where( r => r.Value != null); 
        }

        internal static IDictionary<string, string> GetNameValueStringDictionary(IEnumerable<InfoValuePair> infoValuePairs)
        {
            return Enumerable.ToDictionary(infoValuePairs, a => a.Info.Name,
                a => a.Value.ToUriCompatibleString());
        }

        //internal static RestContractAttribute GetRestContractAttribute(MethodInfo methodInfo)
        //{
        //    return methodInfo.DeclaringType
        //        .GetCustomAttributes(typeof (RestContractAttribute), true)
        //        .Cast<RestContractAttribute>()
        //        .Single();
        //}

        //internal static RouteAttribute GetRestOperationAttribute(MethodInfo methodInfo)
        //{
        //    return methodInfo
        //        .GetCustomAttributes(typeof (RouteAttribute), true)
        //        .Cast<RouteAttribute>()
        //        .Single();
        //}

        internal static IEnumerable<InfoValuePair> UriPatternInfoValuePairs(IEnumerable<InfoValuePair> nameValuePairs, IEnumerable<string> uriPatternParameterNames)
        {
            return nameValuePairs
                .Join(uriPatternParameterNames, p => p.Info.Name, n => n, (p, n) => p);
        }

        private const string Client = "Client";

        private static IObservable<TResult> GetObservableResult<TResult>(
            MethodInfo methodInfo,
            params object[] paramValues)
        {
            //RouteAttribute routeAttribute = GetRestOperationAttribute(methodInfo);
            RestMethodAttribute restMethodAttribute = GetRestAttribute(methodInfo);
           // RestContractAttribute restContractAttribute = GetRestContractAttribute(methodInfo);
            List<ParameterInfo> parameters = methodInfo.GetParameters().ToList();

            var hasRequestParameter = CheckRequestParameter(parameters);

            string serviceClientName = methodInfo.DeclaringType.Name;
            string serviceName = serviceClientName.Substring(0, serviceClientName.Length - Client.Length);
            var serviceUri = new Uri(Application.Current.Host.Source, string.Format("../{0}", serviceName));
            var operationUri = serviceUri.Concat(methodInfo.Name);

            //TODO: currently ingores all parameters with null values. But should though exception if uri parameter is 
            //not supplied.
            IEnumerable<InfoValuePair> nameValuePairs = GetNameValuePairs(parameters, paramValues);
            
            //var uriPatternParameterNames = routeAttribute.UriPattern.GetUriPatternParameterNames().ToList();
            //var uriPatternInfoValuePairs = UriPatternInfoValuePairs(nameValuePairs, uriPatternParameterNames);
            //var uriPatternNameVauleDictionary = GetNameValueStringDictionary(uriPatternInfoValuePairs);

            //operationUri = operationUri.Concat(routeAttribute.UriPattern.InsertUriParameters(uriPatternNameVauleDictionary));

            var queryStringInfoVaulePairs = nameValuePairs
            //    .Except(uriPatternInfoValuePairs, i => i.Info)
                .Where(p => !p.Info.IsDefined(typeof(RequestParameterAttribute), false));
            var queryStringNameVauleDictionary = GetNameValueStringDictionary(queryStringInfoVaulePairs);

            operationUri = operationUri.Concat(CreateQueryString(queryStringNameVauleDictionary));
            
            if (!hasRequestParameter || restMethodAttribute.Method == HttpVerbs.GET)
            {
                return GetObservableResult<TResult>(operationUri, restMethodAttribute.Method);
            }

            object requestParam = nameValuePairs
           //     .Except(uriPatternInfoValuePairs, i => i.Info)
                .Except(queryStringInfoVaulePairs, i => i.Info)
                .Single(p => p.Info.IsDefined(typeof(RequestParameterAttribute),false)).Value;

            return GetObservableResult<TResult>(operationUri, restMethodAttribute.Method, requestParam);
        }

        private static IObservable<TResult> GetObservableResult<TResult>(Uri operationUri, HttpVerbs verb)
        {
            //TODO: Hotfix for incubator. Create smart approach to select what stack will be used.
           // WebRequest webRequest = WebRequestCreator.ClientHttp.Create(operationUri);

            WebRequest webRequest = WebRequestCreator.BrowserHttp.Create(operationUri);
            webRequest.Method = verb.ToString();
            if (verb != HttpVerbs.GET) webRequest.ContentType = JsonContentType;

            return Observable.FromAsyncPattern<WebResponse>(
                webRequest.BeginGetResponse,
                webRequest.EndGetResponse)()
                .Select(
                wr =>
                    {
                        using (Stream responseStream = wr.GetResponseStream())
                        {
                            var serializer = new DataContractJsonSerializer(typeof(TResult));
                            return (TResult)serializer.ReadObject(responseStream);
                        }
                    }
                );
        }

        private static IObservable<TResult> GetObservableResult<TResult>(Uri operationUri, HttpVerbs verb, object requestParam)
        {
            //TODO: Hotfix for incubator. Create smart approach to select what stack will be used.
            // WebRequest webRequest = WebRequestCreator.ClientHttp.Create(operationUri);

            WebRequest webRequest = WebRequestCreator.BrowserHttp.Create(operationUri);

            webRequest.Method = verb.ToString();
            webRequest.ContentType = JsonContentType;
            
            IObservable<TResult> result =
            from x in Observable.FromAsyncPattern<Stream>(webRequest.BeginGetRequestStream, webRequest.EndGetRequestStream)()
               .Select(s =>
               {
                   using (s)
                   {
                       var serializer = new DataContractJsonSerializer(requestParam.GetType());
                       serializer.WriteObject(s, requestParam);
                   }
                   return new Unit();
               })
            from y in (Observable.FromAsyncPattern<WebResponse>(webRequest.BeginGetResponse, webRequest.EndGetResponse)()
               .Select(wr =>
               {
                   using (Stream rs = wr.GetResponseStream())
                       {
                           var serializer = new DataContractJsonSerializer(typeof(TResult));
                           return (TResult)serializer.ReadObject(rs);
                       }
               })
            )
            select y;
            return result;
        }

        private static RestMethodAttribute GetRestAttribute(MethodInfo methodInfo)
        {
            var restMethodAttribute = methodInfo
                .GetCustomAttributes(typeof(RestMethodAttribute), true)
                .SingleOrDefault() as RestMethodAttribute;
            return restMethodAttribute ?? new HttpPostAttribute();
        }

        #endregion
    }
}